# configure-bind.pm
# Copyright (C) 2006  Stephane Alnet
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# 

#
# For more information: http://carrierclass.net/
#
use strict; use warnings;

package CCNCore::Provision::Application::bind;
use base qw(CCNCore::Provision::Base);

use CCN::Text;
use CCNCore::Scripting;
use CCNCore::Home;

=pod

    is_rfc1918($ip)
        Checks whether an IP is in an RFC1918 block,
        and return the IN-ADDR.ARPA prefix if it is.
        
=cut

sub is_rfc1918($)
{
    my $ip = shift;
    return '10'      if $ip =~ /^10\./;
    return '168.192' if $ip =~ /^192\.168\./;
    return "$1.172"  if $ip =~ /^172\.(1[6-9]|2[0-9]|3[01])\./;
    return undef;
}

=pod

    get_serial()
        Returns a Serial usable in an SOA record
        YYY Currently, one serial per ~15 minutes.

=cut

sub get_serial()
{
    my @time = localtime(time);
    my $date = sprintf("%04d%02d%02d", $time[5]+1900, $time[4]+1, $time[3]);
    # Time, in 1/100 of a day. :)
    my $time = sprintf("%02d", ($time[2]*3600+$time[1]*60+$time[0])/865);
    return $date.$time;
}


=pod

    anchor_system()
    
        Should be overwritten by the subclass if it needs to provide additional
        DNS records for the system.

=cut
    
sub anchor_system()
{
    my $self = shift;
    my $dnsconf = '';
    # In subclass, add content here.
    # ...
    # Done, return the additional content.
    return $dnsconf;
}

sub run()
{
    my $self = shift;
    return if $self->p->force_manager;

    $self->PACKAGE ('bind9');
    $self->CHECKFOR ('/etc/init.d/bind9');
    $self->CHECKFOR ('/usr/sbin/named');
    $self->CHECKFOR ('/etc/bind');

    $self->enable('bind');
    $self->enable('bind9'); # bind9 on Debian (at least)

    # Start configuration
    return $self->error ("You must defined a manager for system ".$self$self->p->system_name)
        if not defined $self->p->manager;

    my $SERIAL = get_serial();

    # Yes: create bind primary files + config files
    my $namedconf = <<'EOT';
logging {

  channel stdlog {
    file "/var/log/named/messages" versions 5 size 10M;
    print-category yes;
    print-severity yes;
    print-time yes;
  };

  channel querylog {
    file "/var/log/named/queries" versions 1 size 10M;
    print-time yes;
  };

  category default { stdlog; };
  category queries { querylog; };
  category update { stdlog; };
  category security { stdlog; };
  category notify { stdlog; };

};

EOT

    # ------------------ Manager -> SOA
    # ------------------ Master  -> NS

    my $SYSNAME = $self$self->p->system_name;

    if( $self->p->is_manager or $self->{dns_master} )
    {
        # Generate domain (system-level) zone files.
        $namedconf .= <<"EOT";
zone "${SYSNAME}" {
    type master;
    file "/etc/bind/primary/db.${SYSNAME}";
    allow-query { any; };
    allow-transfer { any; };
    notify yes;
};
EOT

        # Generates Primary file
        our $dnsconf = '';
        our $mastersconf = '';

        # DNS masters => NS records
        $self->sql_do_for( q(SELECT id FROM server WHERE the_system = ? AND dns_master = 'TRUE'), $self->p->system_pk,
        sub
        {
            my $master = $self$self->p->make_server_name(shift);
            $mastersconf .= "\@ IN NS ${master}.\n";
        });

        # Generate A and PTR records for servers in the system.
        my %reverse = ();
        $self->sql_do_for( q(SELECT id FROM server WHERE the_system = ?), $self->p->system_pk,
        sub
        {
            my $server_pk = shift;
            my $ip = $self->primary_ip($server_pk);

            my $server_name = $self$self->p->make_server_name($server_pk);

            $dnsconf .= "${server_name}. IN A ${ip}\n"
                if defined $server_name and defined $ip;

            $self->sql_do_for(<<'SQL', $server_pk,
                SELECT ifname, ipnet_ip, ipnet_mask FROM server_interface 
                WHERE server = ? AND ipnet_ip IS NOT NULL AND ipnet_ip <> ''
SQL
            sub
            {
                my ($intf,$ip,$mask) = @_;
                # We only generate Reverse for RFC1918 areas for now.
                # YYY Need a way to specify whether to generate other areas.
                my $reverse_area = is_rfc1918($ip);
                if( defined $reverse_area )
                {
                    my $reverse = join('.',reverse split(/\./,$ip));
                    $reverse{$reverse_area} .= "${reverse}.IN-ADDR.ARPA.  IN  PTR ${server_name}.\n"
                        if defined $reverse and defined $server_name;
                }
            });
        });

        # Generate XMPP/Jabber SRV records.
        # Currently the lowest-numbered downstream server is used as the jabber server.
        # (Until I figure out how to cluster ejabberd!)
        $self->sql_do_for( <<'SQL', $self->p->system_pk,
            SELECT MIN(cdn_downstream_server) FROM server WHERE the_system = ?
SQL
        sub
        {
            my $sname = $self$self->p->make_server_name(shift);
            $dnsconf .= <<"EOT";
_xmpp-server._tcp.${SYSNAME}.           IN SRV 50 0 5269 ${sname}.
_jabber._tcp.${SYSNAME}.                IN SRV 50 0 5269 ${sname}.
_jabber-server._tcp.${SYSNAME}.         IN SRV 50 0 5269 ${sname}.
_xmpp-server._tcp.server.${SYSNAME}.    IN SRV 50 0 5269 ${sname}.
_jabber._tcp.server.${SYSNAME}.         IN SRV 50 0 5269 ${sname}.
_jabber-server._tcp.server.${SYSNAME}.  IN SRV 50 0 5269 ${sname}.
EOT
        });
        
        $dnsconf .= $self->anchor_system();

        
        # TBD-URGENT Cross-systems DNS entries
        #   . root system (GUI) info in all systems
        #   . other system's info (especially server names) in GUI system
        

        # Main domain
        # Serial
        # Refresh (in seconds) -- interval used by secondaries to check the serial
        # Retry   (in seconds) -- interval for retry if refresh failed
        # Expire  (in seconds) -- interval to discard if retry fail
        # Minimum (in seconds) -- cache ttl for negative answers
        my $manager = $self$self->p->make_server_name($self->p->manager);
        my $primary = <<"EOT";
\$TTL 865
\@   IN  SOA     ${manager}. root.${SYSNAME}. ( ${SERIAL} 120 30 3600000 604800 )
manager IN CNAME ${manager}.
$mastersconf
$dnsconf
EOT

        foreach("/etc/bind/primary/db.${SYSNAME}")
        {
            CAT $_, $primary;
        }

        # IN-ADDR.ARPA
        my $in_addr_arpa = <<"EOT";
\$TTL 865
\@ IN SOA ${manager}. root.${SYSNAME}. ( ${SERIAL} 120 30 3600000 604800 )
$mastersconf
EOT

        $namedconf .= <<"EOT";
zone "IN-ADDR.ARPA" {
    type master;
    file "/etc/bind/primary/db.in-addr.arpa";
    allow-query { any; };
    allow-transfer { any; };
    notify yes;
};
EOT

        foreach("/etc/bind/primary/db.in-addr.arpa")
        {
            CAT $_, $in_addr_arpa;
        }

        for my $rev (keys %reverse)
        {
            $namedconf .= <<"EOT";
zone "$rev.IN-ADDR.ARPA" {
    type master;
    file "/etc/bind/primary/db.$rev";
    allow-query { any; };
    allow-transfer { any; };
    notify yes;
};
EOT

            $primary = <<"EOT" . $reverse{$rev};
\$TTL 865
\@ IN SOA   ${manager}. root.${SYSNAME}. ( ${SERIAL} 120 30 3600000 604800 )
$mastersconf
EOT

            foreach("/etc/bind/primary/db.$rev")
            {
                CAT $_, $primary;
            }
        } # foreach %reverse zone
    }

    # -------------------------- Not a DNS master

    else
    {
        my $masters = '';
        my $hints   = '';

        # DNS masters => NS records
        $self->sql_do_for( q(SELECT id FROM server WHERE the_system = ? AND dns_master = 'TRUE'), $self->p->system_pk,
        sub
        {
            my $master_pk = shift;
            my $master = $self$self->p->make_server_name($master_pk);
            my $ip = $self->primary_ip($master_pk);
            $hints .= "${SYSNAME}. IN NS ${master}.\n${master}. IN A ${ip}\n";
            $masters  .= "${ip}; ";
        });

        $namedconf .= <<"EOT";
zone "${SYSNAME}" {
    type slave;
    masters { $masters };
    file "/etc/bind/secondary/db.${SYSNAME}";
    allow-query { any; };
    allow-transfer { any; };
};

zone "in-addr.arpa" {
    type slave;
    masters { $masters };
    file "/etc/bind/secondary/db.in-addr.arpa";
    allow-query { any; };
    allow-transfer { any; };
};
EOT

        foreach("/etc/bind/secondary/hint.${SYSNAME}")
        {
            CAT $_, $hints;
        }
    } # Not a DNS master

    CAT '/etc/bind/named.conf.local', $namedconf;

    COMMIT_MODULE(ref($self));
}

sub commit($)
{
    my $self = shift;
    my $c = shift;

    if($self->upgrading)
    {
        CCN::Scripting::exec_command(CCNCore::Home::manager.'/bin/upgrade/bind.sh');        
    }
    # Always attempt to restart
    # YYYPORT -u bind
    CCN::Scripting::exec_command '/etc/init.d/bind9 stop; /usr/bin/killall -TERM -u bind; /etc/init.d/bind9 start';
}

1;